package ocbn.remotelab.util.date;

import java.util.Calendar;
import java.util.TimeZone;
import java.util.Vector;
import net.rim.device.api.i18n.SimpleDateFormat;
import ocbn.remotelab.exceptions.DateFormatException;

/**
 * Parses a String according to a supplied SimpleDateFormat object and converts
 * it into a Date object. <strong>Note:</strong> This date parser strictly
 * follows the supplied SimpleDateFormat and expects a String that completely
 * conforms to the format. Failure to conform to the format will cause a
 * DateFormatException or an incorrectly converted Date.
 *
 * @author Ke Li
 */
public class DateParser {


    /**
     * A constant 2D integer array that contains the default values for
     * different date fields when a date is parsed. If the default value of a
     * field is unspecified, its default value will be the value of the field in
     * the system clock at the time of parsing. For all i,
     * FIELD_DEFAULT_VALUES[i][0] represents the field and
     * FIELD_DEFAULT_VALUES[i][1] represents the default value for it.
     */
    private final static int[][] FIELD_DEFAULT_VALUES
            = new int[][]{{Calendar.MILLISECOND, 0}};
    /**
     * A constant String array that contains all the fields (represented in
     * the date format style used by SimpleDateFormat) that cannot be converted
     * to integers
     */
    private final static String[] UNCONVERTIBLE_FIELDS = new String[]{"z"};
    /**
     * The date format parser, which parses the date format string and
     * determines the date fields and separators
     */
    private DateFormatParser formatParser;
    /**
     * The string to be parsed that represents the date
     */
    private String dateString;
    /**
     * A Vector of Strings representing all the date components (the values of
     * date fields) as strings. For example, "Sunday", "February", "4" are all
     * examples of date components representing the values of year, month and
     * day respectively.
     */
    private Vector dateComponentsAsStr;
    /**
     * A Vector of Integers representing all the date components (the values of
     * date fields) as integers. These integers correspond to the values of
     * the constants in Calendar class or are as expected if no constants are
     * available. For example,  the values for Sunday, February, and "4" are
     * Calendar.SUNDAY (= 0), Calendar.FEBRUARY (= 1) and 4.
     */
    private Vector dateComponentsAsInt;
    /**
     * A boolean indicating whether string date components have been found (in
     * other words, whether findDateComponentsAsStrings() has been called)
     */
    private boolean foundDateComponentsAsStr;
    /**
     * A boolean indicating whether integer date components have been found (in
     * other words, whether findDateComponents() has been called)
     */
    private boolean foundDateComponentsAsInt;

    /**
     * Constructs a DateParser from the date format and the string to be parsed.
     *
     * @param format        A SimpleDateFormat object representing the format
     *                      that dateString is in
     * @param dateString    The string that should be parsed
     */
    public DateParser(SimpleDateFormat format, String dateString) {
        formatParser = new DateFormatParser(format);
        this.dateString = dateString;
        foundDateComponentsAsStr = false;
        foundDateComponentsAsInt = false;
    }

    /**
     * Determines whether a date field is convertible to integer.
     *
     * @param field     A String representing the date field
     * @return          A boolean indicating whether the field is convertible
     */
    private static boolean isFieldConvertible(String field) {
        // Iterate through all characters of UNCONVERTIBLE_FIELDS
        for (int i = 0; i < UNCONVERTIBLE_FIELDS.length; i++) {
            // If field contains this character, 
            if (field.indexOf(UNCONVERTIBLE_FIELDS[i]) >= 0) {
                return false;
            }
        }
        return true;
    }

    /**
     * Finds date components (the values of date fields) represented as strings.
     *
     * @throws DateFormatException  Indicate that the string that is parsed
     *                              is in the incorrect format
     */
    private void findDateComponentsAsStrings() throws DateFormatException {
        if (!foundDateComponentsAsStr) {
            Vector separators = formatParser.getFieldSeparators();
            dateComponentsAsStr = new Vector();
            // The starting position of the current date component
            int start;
            // The ending position of the current date component
            int end = 0;

            // Iterate through the list of separators, find them in the date
            // string and store the substring between the separators to
            // dateComponentsAsStr Vector
            for (int i = 1; i < separators.size(); i++) {

                // Find the start of the next date component
                start = end + ((String) separators.elementAt(i - 1)).length();

                if (i >= separators.size() - 1
                        && ((String) separators.elementAt(i)).equals("")) {
                    // This is the last separator and its value is "",
                    // indicating that there is no separator between the last
                    // date component and the end of the string.

                    // Therefore, the end of this date component should be
                    // the end of the string
                    end = dateString.length();
                } else {
                    // This is not the last separator.

                    // Find this separator's position, which is also the end
                    // of this date component
                    end = dateString.indexOf((String) separators.elementAt(i),
                            start);
                }
                if (end >= 0) {
                    // If this separator can be found in the date string,

                    // Add the substring between the last separator and this
                    // separator as a date component
                    dateComponentsAsStr.addElement(dateString.substring(start,
                            end));
                } else {
                    // If this separator cannot be found, 
                    throw new DateFormatException();
                }
            }
            foundDateComponentsAsStr = true;
        }
    }

    /**
     * Gets a date component's numerical value from its string representation.
     * Returns Integer.MIN_VALUE if the numerical value cannot be found. 
     *
     * @param dateComponent     A String representing a date component
     * @param dateField         A String representing a date field
     * @return                  The numerical value of the date component.
     *                          Equals to Integer.MIN_VALUE if there is no
     *                          numerical value for the component. 
     */
    private static int getDateComponentValueFromString(String dateComponent,
            String dateField) {

        // Temporary Calendar object
        Calendar tempCal = Calendar.getInstance();

        // Temporary SimpleDateFormat object. This is used to convert integer
        // values of date components, such as 1, to strings, such as "Monday".
        SimpleDateFormat tempDateFormatter = new SimpleDateFormat(dateField);

        // A 2D integer array containing the range of possible integer values
        // for different date fields that are normally not represented as
        // numbers in the date string, such as the day of the week, month and
        // AM/PM
        int[][] possibleValuesForDateField = new int[Math.max
                (Math.max(Calendar.MONTH, Calendar.DAY_OF_WEEK),
                Calendar.AM_PM) + 1][2];
        // Initializes the array. 
        possibleValuesForDateField[Calendar.MONTH]
                = new int[]{Calendar.JANUARY, Calendar.DECEMBER};
        possibleValuesForDateField[Calendar.DAY_OF_WEEK]
                = new int[]{Calendar.SUNDAY, Calendar.SATURDAY};
        possibleValuesForDateField[Calendar.AM_PM]
                = new int[]{Calendar.AM, Calendar.PM};

        // The type of the current date field represented as an integer, which
        // corresponds to the values of the constants in SimpleDateFormat or
        // Calendar class
        int dateFieldType;
        if (isFieldConvertible(dateField)) {
            try {
                if (tempDateFormatter.getFields().length > 0) {
                    // If there are date fields in dateField string,
                    dateFieldType = tempDateFormatter.getFields()[0];

                    // Find the value of the date field so that the string
                    // representation of the value matches dateComponent.
                    for (int i = possibleValuesForDateField[dateFieldType][0];
                            i <= possibleValuesForDateField[dateFieldType][1];
                            i++) {
                        tempCal.set(dateFieldType, i);
                        if (dateComponent.equalsIgnoreCase
                                (tempDateFormatter.format(tempCal))) {
                            return i;
                        }
                    }
                } else {
                    // If there are no date fields in dateField string,
                    return Integer.MIN_VALUE;
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // There are no entries in possibleValuesForDateField array
                // for the current date field.
                return Integer.MIN_VALUE;
            }
        }
        return Integer.MIN_VALUE;
    }

    /**
     * Finds the date components represented as integers.
     *
     * @throws DateFormatException  Indicate that the string to be parsed is in
     *                              the wrong format
     */
    private void findDateComponents() throws DateFormatException {
        if (!foundDateComponentsAsInt) {
            findDateComponentsAsStrings();
            Vector componentFields = formatParser.getFieldsAsStr();
            dateComponentsAsInt = new Vector();
            for (int i = 0; i < dateComponentsAsStr.size(); i++) {
                try {
                    // Try to convert the current date component string to an
                    // integer.
                    dateComponentsAsInt.addElement(new Integer
                            (Integer.parseInt((String) dateComponentsAsStr
                            .elementAt(i))));
                } catch (NumberFormatException e) {
                    // If the component cannot be converted to an integer,
                    dateComponentsAsInt.addElement(new Integer
                            (getDateComponentValueFromString(
                            (String) dateComponentsAsStr.elementAt(i),
                            (String) componentFields.elementAt(i))));
                }
            }
            foundDateComponentsAsInt = true;
        }
    }

    /**
     * Parses the string and get the corresponding Date object.
     *
     * @return                      A Date object that represents the date in
     *                              the string
     * @throws DateFormatException  Indicate that the string to be parsed is
     *                              in the wrong format
     */
    public Date getDate() throws DateFormatException {
        // Date fields represented as integers
        int[] fields = formatParser.getFieldsAsInt();
        Calendar cal = Calendar.getInstance();
        for(int i = 0; i < FIELD_DEFAULT_VALUES.length; i++) {
            cal.set(FIELD_DEFAULT_VALUES[i][0], FIELD_DEFAULT_VALUES[i][1]);
        }
        int tempNum;
        findDateComponents();
        for (int i = 0; i < fields.length; i++) {
            tempNum = ((Integer) dateComponentsAsInt.elementAt(i)).intValue();
            if (tempNum != Integer.MIN_VALUE) {
                // If the current date component is successfully converted to an
                // integer,
                cal.set(fields[i], tempNum);
            } else if (fields[i] == SimpleDateFormat.TIMEZONE_FIELD) {
                // If the current field represents the time zone,
                cal.setTimeZone(TimeZone.getTimeZone(
                        (String) dateComponentsAsStr.elementAt(i)));
            }
        }
        return new Date(cal.getTime());
    }
}
